﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Diagnostics;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;
using System.Threading.Tasks;
using System.IO;
//using System.Configuration;
using Microsoft.Web.Administration;

namespace DDConfigureARR
{
    class Program
    {
        const string WebFarmName = "dynamicdeploywebfarm";

        public const string CUSTOM_DATE_FORMAT_SHORT = "dMMMyyyy";
        #region Sync constants
        private const string LOGS_CONTAINER_NAME_CONFIG = "com.dynamicdeploy.ddconfigurearr.LogsContainer";
        private static string LOGS_CONTAINER_NAME = "ddconfigurearrlogs";
        #endregion
        #region constants for scheduler


        internal const int TIMEINSECSFORINFINITEINITWAIT = 60;
        internal const int BACKUP_INITIAL_WAIT_MILLIS = 300000;

        public static string DEPLOYMENT_ID = null;
        public static string ROLE_ID = null;
        public static string INSTANCE_ID = null;

        const string LOG_FILE_NAME = "DDConfigureARR.log";

        public static string AppRoot = "";
        public static string RoleRoot = "";
        public static string SitesRoot = "";
        public static readonly RetryPolicy RETRY_POLICY = RetryPolicies.Retry(3, TimeSpan.Zero);
        #endregion
        private static bool ENABLE_ARR = false;

        private static void InitTraceOptions()
        {
            DefaultTraceListener drl;
            drl = (DefaultTraceListener)Trace.Listeners["Default"];
            drl.LogFileName = LOG_FILE_NAME;

            drl.TraceOutputOptions = TraceOptions.DateTime;
            Trace.TraceInformation("=====================================================================================================================================");

            Trace.TraceInformation(string.Format("{0}\t{1} in working directory {2}", DateTime.Now.ToString(), "Starting DDConfigureARR plugin ", Environment.CurrentDirectory));

        }

        private static void InitConfigPublisher()
        {
            Trace.TraceInformation(string.Format("{0}\t{1}", DateTime.Now.ToString(), "Start SetupConfigurationSettingPublisher()"));
            SetupConfigurationSettingPublisher();
            Trace.TraceInformation(string.Format("{0}\t{1}", DateTime.Now.ToString(), "End SetupConfigurationSettingPublisher()"));
            ENABLE_ARR = bool.Parse(GetConfigValueAsString("com.dynamicdeploy.ddconfigurearr.EnableARRConfig"));

        }

        private static void SetupConfigurationSettingPublisher()
        {
            // This code sets up a handler to update CloudStorageAccount instances when their corresponding
            // configuration settings change in the service configuration file.
            Trace.TraceInformation("Setting up configuration setting publishing");
            CloudStorageAccount.SetConfigurationSettingPublisher((configName, configSetter) =>
            {
                // Provide the configSetter with the initial value
                configSetter(RoleEnvironment.GetConfigurationSettingValue(configName));

                RoleEnvironment.Changed += (sender, arg) =>
                {
                    if (arg.Changes.OfType<RoleEnvironmentConfigurationSettingChange>()
                        .Any((change) => (change.ConfigurationSettingName == configName)))
                    {
                        // The corresponding configuration setting has changed, propagate the value
                        if (!configSetter(RoleEnvironment.GetConfigurationSettingValue(configName)))
                        {
                            // In this case, the change to the storage account credentials in the
                            // service configuration is significant enough that the role needs to be
                            // recycled in order to use the latest settings. (for example, the 
                            // endpoint has changed)
                            RoleEnvironment.RequestRecycle();
                        }
                    }
                };
            });
        }

        #region Utility Functions
        public static string GetConfigValueAsString(string configName)
        {
            return (RoleEnvironment.IsAvailable)
                                   ? RoleEnvironment.GetConfigurationSettingValue(configName)
                                   : System.Configuration.ConfigurationManager.AppSettings[configName];

        }

        public static string ExpandKeywords(string buffer)
        {
            if (RoleEnvironment.IsAvailable)
            {
                buffer = buffer.Replace("$approot$", AppRoot);
                buffer = buffer.Replace("$deploymentid$", RoleEnvironment.DeploymentId);
                buffer = buffer.Replace("$roleinstanceid$", RoleEnvironment.CurrentRoleInstance.Id);
                buffer = buffer.Replace("$computername$", Environment.MachineName);
                buffer = buffer.Replace("$guid$", Guid.NewGuid().ToString());
                buffer = buffer.Replace("$now$", DateTime.Now.ToString());
                buffer = buffer.Replace("$roleroot$", Environment.GetEnvironmentVariable("RoleRoot"));

                if (!string.IsNullOrEmpty(SitesRoot))
                {

                    buffer = buffer.Replace("$sitesroot$", SitesRoot);
                }
            }
            return buffer;
        }
        public static void FillEnvironmentVariables()
        {
            if (RoleEnvironment.IsAvailable)
            {
                try
                {
                    RoleRoot = Environment.GetEnvironmentVariable("RoleRoot");

                    AppRoot = Environment.GetEnvironmentVariable("RoleRoot") + @"\approot";
                    SitesRoot = Environment.GetEnvironmentVariable("RoleRoot") + @"\sitesroot";
                    if (!Directory.Exists(SitesRoot))
                    {
                        SitesRoot = null;
                        // May be WorkerRole
                        //AppRoot = Environment.GetEnvironmentVariable("RoleRoot") + @"\approot";
                        //SitesRoot = AppRoot;
                        if (RoleEnvironment.IsEmulated)
                        {

                            SitesRoot = AppRoot;
                        }
                    }

                    Trace.TraceInformation("Sites Root " + ((!string.IsNullOrEmpty(SitesRoot)) ? SitesRoot : "Seems to be a Worker Role"));
                    Trace.TraceInformation("AppRoot " + AppRoot);


                }
                catch (Exception ex)
                {
                    Trace.TraceError("Error retrieving environment variables and config variables in FillEnvironmentVariables() " + ex.Message);

                }
            }
        }

        #endregion

        #region Logs
        static void UploadLogFile()
        {

            try
            {

                string filePath = Path.Combine(Environment.CurrentDirectory, LOG_FILE_NAME);
                if (File.Exists(filePath) && new FileInfo(filePath).Length > 0)
                {
                    string logsContainer = LOGS_CONTAINER_NAME;
                    if (!string.IsNullOrEmpty(GetConfigValueAsString("com.dynamicdeploy.ddconfigurearr.LogsContainer")))
                    {
                        logsContainer = GetConfigValueAsString("com.dynamicdeploy.ddconfigurearr.LogsContainer");
                    }
                    var logs = CloudStorageAccount.Parse(GetConfigValueAsString("com.dynamicdeploy.ddconfigurearr.StorageAccountConnectionString")).CreateCloudBlobClient().GetContainerReference(logsContainer);
                    logs.CreateIfNotExist();
                    string customDT = DateTime.UtcNow.ToString(CUSTOM_DATE_FORMAT_SHORT).Replace(" ", "");
                    var error = logs.GetBlobReference(string.Format("{0}/{1}/{2}/{3}{4}.log", DEPLOYMENT_ID, RoleEnvironment.CurrentRoleInstance.Id, customDT, "ddconfigurearr-", (DateTime.MaxValue - DateTime.UtcNow).Ticks.ToString("d19")));
                    error.Properties.ContentType = "text/plain";


                    Trace.TraceInformation("=====================================================================================================================================");

                    error.UploadFile(filePath, new BlobRequestOptions() { RetryPolicy = RETRY_POLICY, Timeout = TimeSpan.FromSeconds(60) });
                    File.WriteAllText(filePath, string.Empty);
                }
            }
            catch (Exception ex)
            {
                Trace.TraceError("Error uploading log file " + ex.Message);
            }
        }
        #endregion
        static void Main(string[] args)
        {
            try
            {
                InitTraceOptions();
                InitConfigPublisher();
                if (ENABLE_ARR)
                {
                    // Register event handler for roleinstance stopping  and changed events
                    if (RoleEnvironment.IsAvailable)
                    {
                        RoleEnvironment.Stopping += new EventHandler<RoleEnvironmentStoppingEventArgs>(RoleEnvironment_Stopping);
                       
                        DEPLOYMENT_ID = RoleEnvironment.DeploymentId;
                        INSTANCE_ID = RoleEnvironment.CurrentRoleInstance.Id;
                        ROLE_ID = RoleEnvironment.CurrentRoleInstance.Role.Name;
                    }
                    else
                    {
                        DEPLOYMENT_ID = System.Guid.NewGuid().ToString("N");
                        INSTANCE_ID = "1";
                        ROLE_ID = Environment.MachineName;
                    }



                    FillEnvironmentVariables();

              
                    try
                    {




                        while (!ConfigureArrOnce())
                        {
                            Trace.TraceError("There was an error configuring ARR. Will go into infinite loop trying for seconds " + TIMEINSECSFORINFINITEINITWAIT);
                            Thread.Sleep(TimeSpan.FromSeconds(TIMEINSECSFORINFINITEINITWAIT));

                        }



                    }
                    catch (Exception ex)
                    {

                        Trace.TraceError("Error configuring ARR. Quitting..." + ex.Message);

                        // Environment.Exit(-1);
                    }

                    //Keep on checking the drives in a loop
                    while (true)
                    {

                        try
                        {
                          
                            try
                            {
                                Thread.Sleep(TimeSpan.FromSeconds(Program.TIMEINSECSFORINFINITEINITWAIT * 10));
                                UploadLogFile();

                            }
                            catch (Exception ex)
                            {
                                Trace.TraceError("DDConfigureARR fail in Run " + ex.Message);
                            }
                          

                        }
                        catch (Exception ex)
                        {

                            Trace.TraceInformation("Error uploading log file " + ex.Message);
                        }





                    }

                }
                else
                {

                    Trace.TraceInformation("Persistent drives are not enabled.");

                }
               

                RoleEnvironment.Changed += new EventHandler<RoleEnvironmentChangedEventArgs>(RoleEnvironment_Changed);

            }
            catch (Exception ex)
            {

                Console.WriteLine("Error " + ex.Message);
            }
        }


        #region ARR Configuration
        static ConfigurationElement GetOrCreateArrFarmConfigurationElement(ServerManager sm)
        {
            ConfigurationElement farm = null;

            var config = sm.GetApplicationHostConfiguration();

            var farmsSection = config.GetSection("webFarms");
            var farms = farmsSection.GetCollection();

            try
            {
                foreach (var x in farms)
                {
                    if (x.Attributes["name"].Value.ToString() == WebFarmName)
                    {
                        farm = x;
                        break;
                    }
                }
                if (farm == null || farm.GetAttributeValue("name").ToString() != WebFarmName)
                    throw new ApplicationException("Not the right farm");
            }
            catch (Exception ex)
            {
                Trace.WriteLine(string.Format("ConfigureArrOnce - create webFarm element (exception was {0})", ex));
                farm = farms.CreateElement("webFarm");
                farm.SetAttributeValue("name", WebFarmName);
                farm.SetAttributeValue("enabled", "true");
                farms.Add(farm);

                Trace.WriteLine("Setting affinity");
                farm.GetChildElement("applicationRequestRouting").GetChildElement("affinity").SetAttributeValue("useCookie", true);

                Trace.WriteLine("Setting load balancing algorithm");
                farm.GetChildElement("applicationRequestRouting").GetChildElement("loadBalancing").SetAttributeValue("algorithm", "WeightedRoundRobin");

            }

            Trace.WriteLine(string.Format("GetOrCreateArrFarmConfigurationElement - will return {0}", farm));

            return farm;
        }

       static bool ConfigureArrOnce()
        {
            Trace.WriteLine("Configuring ARR");
            bool allIsWell = true;

            //configuration in applicationHost should look like this
            //<webFarms>
            //  <applicationRequestRouting>
            //    <hostAffinityProviderList>
            //        <add name="Microsoft.Web.Arr.HostNameRoundRobin" />
            //        <add name="Microsoft.Web.Arr.HostNameMemory" />
            //    </hostAffinityProviderList>
            //  </applicationRequestRouting>
            //</webFarms>
            //</configuration>



            // Create Server Farm in IIS
            // and add servers to it

            try
            {
                allIsWell = UpdateArr();

                if (allIsWell)
                {

                    using (ServerManager sm = new ServerManager())
                    {
                        try
                        {
                            var config = sm.GetApplicationHostConfiguration();
                            var rules = config.GetSection("system.webServer/rewrite/rules");
                            var rule = rules.GetCollection().CreateElement("rule");
                            rule.SetAttributeValue("name", WebFarmName);
                            rule.SetAttributeValue("stopProcessing", true);
                            rules.GetCollection().Add(rule);
                            rule.GetChildElement("match").SetAttributeValue("url", ".*");
                            rule.GetChildElement("action").SetAttributeValue("type", "Rewrite");
                            rule.GetChildElement("action").SetAttributeValue("url", "http://" + WebFarmName + @"/{R:0}");
                        }
                        catch (Exception ex)
                        {
                            Trace.WriteLine(string.Format("Exception. Assuming rewrite rule already exists. Exception was {0}", ex));
                        }

                        // set the default AppPool to Classic mode
                        var appPool = sm.ApplicationPools["DefaultAppPool"];
                        appPool.ManagedPipelineMode = ManagedPipelineMode.Classic;

                        string localIpv4Address = Dns.GetHostAddresses(Dns.GetHostName())
                            .First(ip => ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork).ToString();

                        foreach (var binding in sm.Sites["Default Web Site"].Bindings)
                        {
                            if (binding.Protocol == "http")
                            {
                                binding["bindingInformation"] = string.Format("{0}:80:", localIpv4Address);
                            } else if (binding.Protocol == "https")
                            {
                                binding["bindingInformation"] = string.Format("{0}:443:", localIpv4Address);
                            }
                        }


                        sm.CommitChanges();
                        Trace.WriteLine("Committed Changes to ARR");

                    } // dispose ServerManager instance
                }//alliswell

            }
            catch (Exception ex)
            {
                Trace.TraceError("Error in ConfigureArrOnce() " + ex.Message);
                allIsWell = false;
            }

            return allIsWell;
        }

       static void RoleEnvironment_Changed(object sender, RoleEnvironmentChangedEventArgs e)
       {
           try
           {
               Trace.WriteLine(string.Format("RoleEnvironment_Changed {0}", e.Changes.Count));
               if (e.Changes.Any(chg => chg is RoleEnvironmentTopologyChange))
               {
                   Trace.WriteLine("RoleEnvironmentTopologyChange");
                   UpdateArr();
               }
           }
           catch (Exception ex)
           {
               Trace.WriteLine(string.Format("Exception on RoleEnvironment_Changed: '{0}'", ex));
               throw ex;
           }
       }

       static void RoleEnvironment_Stopping(object sender, RoleEnvironmentStoppingEventArgs e)
       {
           try
           {
               Trace.TraceWarning("Roleinstance stopping.");



           }
           catch (Exception ex)
           {
               Trace.TraceError("Error in RoleEnvironment_Stopping " + ex.Message);
           }
           finally
           {

               UploadLogFile();
           }
           Environment.Exit(-1);
       }
        /// <summary>
        /// Will add missing nodes to the farm, and remove nodes that don't exist anymore
        /// </summary>
        /// <param name="farm"></param>
       static bool UpdateArr()
        {
            using (ServerManager sm = new ServerManager())
            {
                var farm = GetOrCreateArrFarmConfigurationElement(sm);

                string webFarmRoleName = GetConfigValueAsString("com.dynamicdeploy.ddconfigurearr.WebFarmRoleName");
                if (webFarmRoleName == null)
                {
                    Trace.WriteLine("No ARR configuration as WebFarmRoleName is not defined in configuration");
                    return false;
                }
                else
                {
                    Trace.WriteLine("WebFarmRoleName:" + webFarmRoleName);

                }

                string webFarmEndpointName = GetConfigValueAsString("com.dynamicdeploy.ddconfigurearr.WebFarmEndpointName");
                if (webFarmEndpointName == null)
                {
                    Trace.WriteLine("No ARR configuration as WebFarmEndpointName is not defined in configuration");
                    return false;
                }
                else
                {
                    Trace.WriteLine("WebFarmEndpointName:" + webFarmEndpointName);

                }

                Role webfarmRole = RoleEnvironment.Roles[webFarmRoleName];
                if (webfarmRole == null)
                {
                    Trace.WriteLine(
                        string.Format("No ARR configuration because no role named '{0}' was found in role environment",
                        webFarmRoleName));
                    return false;
                }

                // get the list of nodes in the Web Farm Role
                Dictionary<string, IPEndPoint> webFarmRoleNodes = new Dictionary<string, IPEndPoint>();
                foreach (RoleInstance instance in webfarmRole.Instances)
                {
                    Trace.WriteLine(string.Format("Role instance {0}", instance.Id));

                    foreach (var endpoint in instance.InstanceEndpoints.Where(e => e.Key == webFarmEndpointName))
                    {
                        string nodeKey = string.Format("{0}", endpoint.Value.IPEndpoint.Address.ToString());
                        webFarmRoleNodes.Add(nodeKey, endpoint.Value.IPEndpoint);
                        Trace.WriteLine(string.Format("WebFarmRoleNode: {0}:{1}", nodeKey, endpoint.Value.IPEndpoint.Port));
                    }
                }

                var arrServers = farm.GetCollection();
                // update ARR
                // 1) remove ARR nodes that do not correspond to a node anymore 
                List<ConfigurationElement> serversToRemove = new List<ConfigurationElement>();
                try
                {
                    foreach (var arrServer in arrServers)
                    {
                        if (webFarmRoleNodes.Count(n => n.Key == arrServer.GetAttributeValue("address").ToString()) == 0)
                        {
                            serversToRemove.Add(arrServer);
                        }
                    }

                    foreach (var x in serversToRemove)
                    {
                        Trace.WriteLine(string.Format("Will remove server {0} from the ARR farm", x.GetAttributeValue("address")));
                        arrServers.Remove(x);
                    }
                }
                catch (Exception ex)
                {
                    Trace.WriteLine(string.Format("Exception. Assuming there is no server in the farm. Exception was {0}", ex));
                }

                // 2) add missing nodes in the ARR configuration
                foreach (var node in webFarmRoleNodes)
                {
                    int existingServersCount = 0;
                    try
                    {
                        existingServersCount = arrServers.Count(c => c.GetAttributeValue("address").ToString() == node.Key);
                        Trace.WriteLine(string.Format("Existing Server Count {0}", existingServersCount));
                    }
                    catch (Exception ex)
                    {
                        Trace.WriteLine(string.Format("Exception. Assuming there is no server. Exception was {0}", ex));
                        existingServersCount = 0;
                    }

                    if (existingServersCount == 0)
                    {
                        var server = arrServers.CreateElement("server");
                        server.SetAttributeValue("address", node.Key);
                        server.SetAttributeValue("enabled", true);
                        Trace.WriteLine(string.Format("Will add server {0} to the ARR farm", node.Key));
                        arrServers.Add(server);
                        server.GetChildElement("applicationRequestRouting").SetAttributeValue("httpPort", node.Value.Port);
                    }
                }

                sm.CommitChanges();
                Trace.WriteLine("Committed Changes to ARR");
            }// dispose ServerManager instance

            return true;
        }

        #endregion
    }
}
